// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: donggu

#pragma once

#include <map>
#include <string>
#include <unordered_map>

#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/stacktrace.hpp>
#include <rapidjson/document.h>

#include "coin/proto/coin_market_enums.pb.h"
#include "coin2/base/time.h"
#include "coin2/base/conversion.h"
#include "coin2/exchange/base/feed/update.h"
#include "coin2/exchange/base/feed_util/book_builder.h"
#include "coin2/exchange/base/feed_util/trade.h"
#include "coin2/exchange/base/market/enums.h"

namespace impl {
using coin2::exchange::base::market::ExchangeType;
using coin2::exchange::base::market::MarketType;
using coin2::exchange::base::symbology::IProductHolder;
using namespace coin::proto;

/**
 * orderBookL2 insert  (id,side,size,priec)
 * orderBookL2 update  (id,side,size)
 * orderBookL2 delete  (id,side)
 * orderBookL2 partial (id,side,size,price)
 *
 * orderBookL2_25 insert (id,side,size,price)
 * orderBookL2_25 update (id,side,size)
 * orderBookL2_25 delete (id,side)
 * orderBookL2_25 partial ?
 *
 * orderBook10 insert ?
 * orderBook10 update (bids[[p,q]], asks, timestamp)
 * orderBook10 delete
 * orderBook10 partial (bids[[p,q]], asks, timestamp)
 *
 * trade insert ?
 */

class BitmexFeedUpdateParser {
 public:
  int ParseL2BookPartialItem(const std::string& native_symbol, BookBuilder* book_builder,
                             int64_t timestamp, const rapidjson::Value& arr_val,
                             rapidjson::SizeType idx) {
    const auto& arr = arr_val.GetArray();
    auto tx = book_builder->NewTransaction(timestamp);
    tx.Clear();
    rapidjson::SizeType i;
    for (i = idx; i < arr.Size() && arr[i]["symbol"].GetString() == native_symbol; i++) {
      double p = arr[i]["price"].GetDouble();
      double q = arr[i]["size"].GetDouble();
      const std::string& side = arr[i]["side"].GetString();
      if (side == "Sell") {
        tx.UpdateAsk(p, q);
      } else if (side == "Buy") {
        tx.UpdateBid(p, q);
      } else {
        THROW() << "Unknown tick side: " << side;
      }
    }
    return i;
  }

  int ParseL2BookInsertItem(const std::string& native_symbol, BookBuilder* book_builder,
                            int64_t timestamp, const rapidjson::Value& arr_val,
                            rapidjson::SizeType idx, bool light_bbo) {
    const auto& arr = arr_val.GetArray();
    auto tx = book_builder->NewTransaction(timestamp);
    if (light_bbo) tx.BboClear();
    rapidjson::SizeType i;
    for (i = idx; i < arr.Size() && arr[i]["symbol"].GetString() == native_symbol; i++) {
      double p = arr[i]["price"].GetDouble();
      double q = arr[i]["size"].GetDouble();
      const std::string& side = arr[i]["side"].GetString();
      if (side == "Sell") {
        tx.UpdateAsk(p, q);
      } else if (side == "Buy") {
        tx.UpdateBid(p, q);
      } else {
        THROW() << "Unknown tick side: " << side;
      }
    }
    return i;
  }
  int ParseL2BookDeleteItem(
      const std::string& native_symbol,
      const std::string& relative_norm,
      BookBuilder* book_builder,
      int64_t timestamp,
      const rapidjson::Value& arr_val,
      rapidjson::SizeType idx,
      bool* discard,
      bool light_bbo,
      std::unordered_map<std::string, std::unique_ptr<IProductHolder>>& native_to_pi) const {
    const auto& arr = arr_val.GetArray();
    auto tx = book_builder->NewTransaction(timestamp);
    if (light_bbo) tx.BboClear();
    rapidjson::SizeType i;
    for (i = idx; i < arr.Size() && arr[i]["symbol"].GetString() == native_symbol; i++) {
      int64_t id = arr[i]["id"].GetInt64();
      double p;
      if (!ComputePrice(native_symbol, relative_norm, id, &p, native_to_pi)) {
        continue;
      }
      *discard = false;
      const std::string& side = arr[i]["side"].GetString();
      if (side == "Sell") {
        tx.UpdateAsk(p, 0);
      } else if (side == "Buy") {
        tx.UpdateBid(p, 0);
      } else {
        THROW() << "Unknown tick side: " << side;
      }
    }
    return i;
  }
  int ParseL2BookUpdateItem(
      const std::string& native_symbol,
      const std::string& relative_norm,
      BookBuilder* book_builder,
      int64_t timestamp,
      const rapidjson::Value& arr_val,
      rapidjson::SizeType idx,
      bool* discard,
      bool light_bbo,
      std::unordered_map<std::string, std::unique_ptr<IProductHolder>>& native_to_pi) const {
    const auto& arr = arr_val.GetArray();

    auto tx = book_builder->NewTransaction(timestamp);
    if (light_bbo) tx.BboClear();
    rapidjson::SizeType i;
    for (i = idx; i < arr.Size() && arr[i]["symbol"].GetString() == native_symbol; i++) {
      int64_t id = arr[i]["id"].GetInt64();
      double p;
      if (!ComputePrice(native_symbol, relative_norm, id, &p, native_to_pi)) {
        continue;
      }
      *discard = false;
      double q = arr[i]["size"].GetDouble();
      const std::string& side = arr[i]["side"].GetString();
      if (side == "Sell") {
        tx.UpdateAsk(p, q);
      } else if (side == "Buy") {
        tx.UpdateBid(p, q);
      } else {
        THROW() << "Unknown tick side: " << side;
      }
    }
    return i;
  }

  /////////////

  int Parse10BookPartialItem(const std::string& native_symbol_, BookBuilder* book_builder,
                             int64_t timestamp, const rapidjson::Value& arr_val,
                             rapidjson::SizeType idx) {
    const auto& arr = arr_val.GetArray();
    auto tx = book_builder->NewTransaction(timestamp);
    tx.Clear();
    rapidjson::SizeType i;
    for (i = idx; i < arr.Size() && arr[i]["symbol"].GetString() == native_symbol_; i++) {
      for (const auto& item : arr[i]["bids"].GetArray()) {
        double p = item[0].GetDouble();
        double q = item[1].GetDouble();
        tx.UpdateBid(p, q);
      }
      for (const auto& item : arr[i]["asks"].GetArray()) {
        double p = item[0].GetDouble();
        double q = item[1].GetDouble();
        tx.UpdateAsk(p, q);
      }
    }
    return i;
  }
  int Parse10BookUpdateItem(const std::string& native_symbol_, BookBuilder* book_builder,
                            int64_t timestamp, const rapidjson::Value& arr_val,
                            rapidjson::SizeType idx, bool light_bbo) {
    const auto& arr = arr_val.GetArray();
    auto tx = book_builder->NewTransaction(timestamp);
    if (light_bbo) tx.BboClear();
    rapidjson::SizeType i;
    for (i = idx; i < arr.Size() && arr[i]["symbol"].GetString() == native_symbol_; i++) {
      for (const auto& item : arr[i]["bids"].GetArray()) {
        double p = item[0].GetDouble();
        double q = item[1].GetDouble();
        tx.UpdateBid(p, q);
      }
      for (const auto& item : arr[i]["asks"].GetArray()) {
        double p = item[0].GetDouble();
        double q = item[1].GetDouble();
        tx.UpdateAsk(p, q);
      }
    }
    return i;
  }

  //////////// Trade

  static bool ParseTradeItem(const rapidjson::Value& trade_json, Trade* trade) {
    // 2019-11-02T10:12:21Z
    // 2019-02-03T00:33:29.410Z
    if (!trade_json.HasMember("side")){
      return false;
    }
    coin2::exchange::feed::TradeSide side = ParseTradeSide(trade_json["side"].GetString());
    if (side == coin2::exchange::feed::TRADE_NO_SIDE) {
      return false;
    }
    trade->side = side;
    trade->timestamp = TimestampFromIso8601(trade_json["timestamp"].GetString());
    trade->price = trade_json["price"].GetDouble();
    trade->fill_qty = trade_json["size"].GetDouble();
    return true;
  }

  static coin2::exchange::feed::TradeSide ParseTradeSide(const std::string& side) {
    if (side == "Buy") {
      return coin2::exchange::feed::TRADE_BUY_SIDE;
    } else if (side == "Sell") {
      return coin2::exchange::feed::TRADE_SELL_SIDE;
    } else if (side.empty()) {
      return coin2::exchange::feed::TRADE_NO_SIDE;
    } else {
      throw std::runtime_error("[Bitmex] unknown side: " + side);
    }
  }

  bool ComputePrice(
      const std::string& native_symbol,
      const std::string& norm_symbol,
      int64_t id,
      double* price,
      std::unordered_map<std::string, std::unique_ptr<IProductHolder>>& native_to_pi) const {
    // https://www.bitmex.com/app/restAPI#OrderBookL2
    // ID = (100000000 * symbolIdx) - (price / instrumentTickSize)
    // price = ((100000000 * symbolIdx) - ID) * instrumentTickSize

    int64_t symbol_id = (id / 100000000) + 1;
    int64_t price_idx = (100000000 - id % 100000000);
    // LOG(INFO) << native_symbol_ << " " << id << " " << symbol_id << " " << price_idx;
    if (symbol_id == 88 && native_symbol == "XBTUSD") {
      *price = price_idx * 0.01;
      return true;
    } else if (symbol_id == 297 && native_symbol == "ETHUSD") {
      *price = price_idx * 0.05;
      return true;
    } else if (symbol_id == 366 && norm_symbol == "ADA-BTC.QUARTER") {
      *price = price_idx * 1e-08;
      return true;
    } else if (symbol_id == 344 && norm_symbol == "BTC-USD.QUARTER") {
      *price = price_idx * 0.5;
      return true;
    } else if (symbol_id == 367 && norm_symbol == "EOS-BTC.QUARTER") {
      *price = price_idx * 1e-07;
      return true;
    } else if (symbol_id == 362 && norm_symbol == "ETH-BTC.QUARTER") {
      *price = price_idx * 1e-05;
      return true;
    } else if (symbol_id == 363 && norm_symbol == "LTC-BTC.QUARTER") {
      *price = price_idx * 5e-06;
      return true;
    } else if (symbol_id == 368 && norm_symbol == "TRX-BTC.QUARTER") {
      *price = price_idx * 1e-08;
      return true;
    } else if (symbol_id == 364 && norm_symbol == "XRP-BTC.QUARTER") {
      *price = price_idx * 1e-08;
      return true;
    } else if (symbol_id == 361 && norm_symbol == "BTC-USD.NEXT_QUARTER") {
      *price = price_idx * 0.5;
      return true;
    } else {
      try {
        const auto* ph = native_to_pi.at(native_symbol).get();
        const auto& pi = ph->product_info();
        *price = price_idx * pi.first_tick_size();
        //LOG(INFO) << native_symbol << ":" << *price << ":" << pi.first_tick_size();
        return true;
      } catch (...) {
        LOG(ERROR) << native_symbol << "/" << id;
        return false;
      }
    }
  }

  static bool ParseFundingRate(
      const rapidjson::Value& instrument_json,
      FundingRate* funding_rate,
      int64_t ts,
      bool is_snapshot) {
    CHECK(funding_rate);
    if (is_snapshot) {
      funding_rate->Clear();
    } else if (funding_rate->timestamp == 0) {
      return false;
    }
    funding_rate->timestamp = ts;
    try {
      if (instrument_json.HasMember("fundingRate")) {
        funding_rate->funding_rate = instrument_json["fundingRate"].GetDouble();
      }
      if (instrument_json.HasMember("indicativeFundingRate")) {
        funding_rate->estimated_rate =
            instrument_json["indicativeFundingRate"].GetDouble();
      }
      if (instrument_json.HasMember("fundingTimestamp")) {
        funding_rate->funding_time =
            TimestampFromIso8601(instrument_json["fundingTimestamp"].GetString());
      }
    } catch(...) {
      // For non perpetual products, funding rate related fields are empty.
      return false;
    }
    return true;
  }

  static fastfeed::proto::KlineInterval ParseKlineInterval(const std::string& period) {
    static std::map<std::string, fastfeed::proto::KlineInterval> interval_map = {
      {"1m", fastfeed::proto::KLINE_INTERVAL_1MIN},
      {"5m", fastfeed::proto::KLINE_INTERVAL_5MIN},
      {"1h", fastfeed::proto::KLINE_INTERVAL_1HOUR},
      {"1d", fastfeed::proto::KLINE_INTERVAL_1DAY}
    };
    auto iter = interval_map.find(period);
    if (iter == interval_map.end()) {
      NOTREACHED() << "Not support interval " << period;
      return fastfeed::proto::KLINE_INTERVAL_INVALID;
    }

    return iter->second;
  }

    static bool Parse1Kline(
      const rapidjson::Value& kline_data,
      Klines* klines,
      int64_t timestamp,
      fastfeed::proto::KlineInterval interval) {
    int64_t open_timestamp = TimestampFromIso8601(kline_data["timestamp"].GetString());
    auto result = klines->AddKline(open_timestamp);
    if (!result) return false;

    Kline* kline;
    bool freezed;
    std::tie(kline, freezed) = result.value();

    kline->open = kline_data["open"].GetDouble();
    kline->high = kline_data["high"].GetDouble();
    kline->low = kline_data["low"].GetDouble();
    kline->close = kline_data["close"].GetDouble();
    kline->volume = kline_data["volume"].GetDouble();
    kline->timestamp = timestamp;
    kline->interval = interval;
    return freezed;
  }
};

}  // namespace impl

using impl::BitmexFeedUpdateParser;
